﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SDCloudSolution.SDCloud.Entities.Models.Data;
using SDCloudSolution.SDCloud.ModelRunAgent.Infrastructure.Interfaces;
using SDCloudSolution.SDCloud.ServiceAgent;

namespace SDCloudSolution.SDCloud.ModelRunAgent.Infrastructure.Engines
{
    public class PysdModelExecutor : BaseModelExecutor
    {
        private readonly ServiceManager _pyServiceManager;

        private const string RunXmileModel = "/api/pysd/xmile/run";

        private const string RunVensimModel = "/api/pysd/vensim/run";

        public PysdModelExecutor(IModelExecutionContextProvider contextProvider, IPysdServiceSettingsProvider settingsProvider)
            : base(contextProvider)
        {
            _pyServiceManager = new ServiceManager(
                settingsProvider.PythonExecutionPath,
                settingsProvider.PythonExecutionArguments,
                settingsProvider.PythonServiceUrl,
                settingsProvider.PythonServicePorts,
                settingsProvider.PythonServiceTimeout
            );
        }

        private static T GetValue<T>(JToken jToken, string key, T defaultValue = default(T))
        {
            dynamic ret = jToken[key];
            if (ret == null) return defaultValue;
            if (ret is JObject) return JsonConvert.DeserializeObject<T>(ret.ToString());
            return (T)ret;
        }

        private static Dictionary<string, double> GetValues(string[] columnNames, double[] values)
        {
            var result = new Dictionary<string, double>();
            var index = 1;
            foreach (var columnName in columnNames.Skip(1))
            {
                result.Add(columnName, values.ElementAt(index++));
            }
            return result;
        }

        // No need to split into several methods due to one-time usage of internal code blocks & common logis through all code block
        private ModelResultDataEntity ConvertToModelResultEntity(string jsonModel)
        {
            var result = new ModelResultDataEntity();

            JObject model = JObject.Parse(jsonModel);
            var columnNames = model.Properties().Select(property => property.Name).ToArray();
            result.Columns = columnNames
                .Where(name => name != "index")
                .Select(name => new ModelResultColumnEntity
                {
                    Name = name
                }).ToArray();

            var timeFrames = new List<ModelResultTimeFrameEntity>();

            var fullList = new List<List<double>>();
            foreach (JToken child in model.Children())
            {
                // read all json values
                foreach (JToken child_child in child.Children())
                {
                    var columnValuesList = new List<double>();
                    for (var i = 0; i < child.Values().Count(); i++)
                    {
                        columnValuesList.Add(GetValue<double>(child_child, i.ToString(), 0));
                    }
                    fullList.Add(columnValuesList);
                }
            }

            if (!fullList.Any()) return result;
            double? initialTime = null;
            double previousTime = 0;
            for (var i = 0; i < fullList.First().Count; i++)
            {
                var currentTime = fullList.First().ElementAt(i);
                if (!initialTime.HasValue)
                {
                    initialTime = currentTime;
                }

                var valuesArray = fullList.Select(list => list.ElementAt(i)).ToArray();
                var timeFrame = new ModelResultTimeFrameEntity
                {
                    Time = currentTime,
                    TimeDelta = currentTime - previousTime,
                    Values = GetValues(columnNames, valuesArray)
                };
                previousTime = currentTime;
                timeFrames.Add(timeFrame);
            }

            result.TimeFrames = timeFrames.ToArray();
            result.TimeStart = initialTime ?? 0;
            result.TimeEnd = previousTime;

            return result;
        }

        public override Task<ModelResultDataEntity> ExecuteModel(string modelFullFilePath)
        {
            return GetModelResultsAsync(modelFullFilePath)
                   .ContinueWith(data => ConvertToModelResultEntity(data.Result));
        }

        private Task<string> GetModelResultsAsync(string modelFullFilePath)
        {
            try
            {
                var extenstion = Path.GetExtension(modelFullFilePath);
                if (extenstion == ".xmile")
                {
                    return _pyServiceManager
                        .GetResponse($"{RunXmileModel}?modelPath={WebUtility.UrlEncode(modelFullFilePath)}");
                }

                if (extenstion == ".mdl")
                {
                    return _pyServiceManager
                        .GetResponse($"{RunVensimModel}?modelPath={WebUtility.UrlEncode(modelFullFilePath)}");
                }

                throw new NotSupportedException("This model format not supported by execution engine");
            }
            catch (ServiceException serviceException)
            {
                if (serviceException.IsExpected)
                {
                    var message = GetValue<string>(JObject.Parse(serviceException.InternalServiceMessage), "message", string.Empty);
                    throw new ServiceException(message, serviceException);
                }
                throw;
            }
        }
    }
}
